package com.google.code.jtasks.client.ui;

import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.googlecode.lanterna.input.Key;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.screen.ScreenCharacterStyle;
import com.googlecode.lanterna.screen.ScreenWriter;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * Вспомогательный класс, взаимодействующий с классом {@link Screen} библиотеки Lanterna и предоставляющий
 * более сложные паттерны взаимодействия с пользователем, например, ввод строк.
 */
public class ScreenHelper {
    private final Screen screen;
    private final ScreenWriter writer;

    @Inject
    ScreenHelper(Screen screen, ScreenWriter writer) {
        this.screen = screen;
        this.writer = writer;
    }

    /**
     * Возвращает объект {@link Screen}. Этот метод нужен, чтобы избежать лишних инжекций в клиентские классы.
     */
    public Screen screen() {
        return screen;
    }

    /**
     * Возвращает объект {@link ScreenWriter}. Этот метод нужен, чтобы избежать лишних инжекций в клиентские классы.
     */
    public ScreenWriter writer() {
        return writer;
    }

    /**
     * Делегирует вызов метода {@link Screen#clear()} текущему объекту {@link Screen}.
     *
     * @see Screen#clear()
     */
    public void clear() {
        screen.clear();
    }

    /**
     * Делегирует вызов метода {@link Screen#refresh()} текущему объекту {@link Screen}.
     *
     * @see Screen#refresh()
     */
    public void refresh() {
        screen.refresh();
    }

    /**
     * Делегирует вызов метода {@link ScreenWriter#drawString(int, int, String, ScreenCharacterStyle...)}
     * текущему объекту {@link ScreenWriter}.
     *
     * @see ScreenWriter#drawString(int, int, String, ScreenCharacterStyle...)
     */
    public void drawString(int x, int y, String string) {
        writer.drawString(x, y, string);
    }

    /**
     * Делегирует вызов метода {@link Screen#setCursorPosition(int, int)} текущему объекту {@link Screen}.
     *
     * @see Screen#setCursorPosition(int, int)
     */
    public void moveCursorTo(int x, int y) {
        screen.setCursorPosition(x, y);
    }

    /**
     * Блокирующее чтение клавиши с клавиатуры. Будет ждать, пока пользователь не нажмёт клавишу на терминале.
     * @return нажатая клавиша
     */
    public Key readKey() {
        Key key = null;
        while (key == null) {
            key = screen.readInput();
        }
        return key;
    }

    /**
     * Очищает прямоугольник, заданный отступами своих сторон (включительно), то есть заполняет его пробелами.
     *
     * @param xfrom отступ левой стороны
     * @param xto отступ правой стороны
     * @param yfrom отступ верхней стороны
     * @param yto отступ нижней стороны
     */
    public void clear(int xfrom, int xto, int yfrom, int yto) {
        checkArgument(xfrom <= xto, "Invalid horizontal boundaries");
        checkArgument(yfrom <= yto, "Invalid vertical boundaries");

        for (int j = xfrom; j <= xto; ++j) {
            writer.drawString(j, yfrom, Strings.repeat(" ", yto-yfrom+1));
        }
        screen.refresh();
    }

    /**
     * Очищает заданную строку целиком (заполняет её пробелами).
     *
     * @param y смещение строки по вертикали
     */
    public void clearLine(int y) {
        clear(0, screen.getTerminalSize().getColumns() - 1, y, y);
    }

    /**
     * Считывает с клавиатуры строку символов с заданным значением по умолчанию.
     *
     * <p>Курсор терминала переводится в положение (x, y), печатается заданная строка по умолчанию, и ожидается
     * ввод символа. При вводе очередного символа курсор соответствующим образом сдвигается (получается иллюзия
     * поля ввода). Поддерживается нажатие Backspace. Enter завершает ввод, введённая строка будет возвращена
     * как {@code Optional.of(<строка>)}. Escape отменяет ввод; тогда будет возвращено {@code Optional.absent()}.</p>
     *
     * @param x смещение по горизонтали начальной позиции курсора
     * @param y смещение по вертикали начальной позиции курсора
     * @param def значение в поле ввода по умолчанию
     * @return {@code Optional.of(значение)}, если был нажат Enter, {@code Optional.absent()} если был нажат Escape
     * @see #readStringWithOutputAt(int, int)
     */
    public Optional<String> readStringWithOutputAt(int x, int y, String def) {
        StringBuilder sb = new StringBuilder(def);

        int d = sb.length(), prevSize = sb.length();
        screen.setCursorPosition(x + d, y);
        writer.drawString(x, y, sb.toString());
        screen.refresh();
        Key key = readKey();
        loop: while (true) {
            switch (key.getKind()) {
                case NormalKey:
                    sb.append(key.getCharacter());
                    screen.setCursorPosition(x + d + 1, y);
                    ++d;
                    break;

                case Backspace:
                    if (d == 0) {
                        break;
                    }
                    sb.delete(sb.length() - 1, sb.length());
                    screen.setCursorPosition(x + d - 1, y);
                    --d;
                    break;

                case Escape:
                    return Optional.absent();

                case Enter:
                    break loop;
            }
            clear(x, x + prevSize, y, y);
            prevSize = d;
            writer.drawString(x, y, sb.toString());
            screen.refresh();

            key = readKey();
        }

        return Optional.of(sb.toString());
    }

    /**
     * Аналогично {@link #readStringWithOutputAt(int, int, String)}, где значение по умолчанию есть пустая строка.
     *
     * @param x смещение по горизонтали начальной позиции курсора
     * @param y смещение по вертикали начальной позиции курсора
     * @return {@code Optional.of(значение)}, если был нажат Enter, {@code Optional.absent()} если был нажат Escape
     */
    public Optional<String> readStringWithOutputAt(int x, int y) {
        return readStringWithOutputAt(x, y, "");
    }

    /**
     * Выводит по заданным координатам указанное сообщение и ждёт нажатия клавиши пользователем. Если была нажата
     * клавиша 'y' или 'Y', то возвращает {@code true}; если была нажата любая другая клавиша, возвращает
     * {@code false}.
     *
     * @param x смещение по горизонтали точки вывода сообщения
     * @param y смещение по вертикали точки вывода сообщения
     * @param message само сообщение
     * @return {@code true}, если пользователь нажат 'y' или 'Y', {@code false} в противном случае
     */
    public boolean ask(int x, int y, String message) {
        message = message + " (y/n) ";
        writer.drawString(x, y, message);
        screen.setCursorPosition(x + message.length(), y);
        screen.refresh();

        char ch = readKey().getCharacter();

        return ch == 'y' || ch == 'Y';
    }

    /**
     * Печатает информационное сообщение и ожидает нажатия любой клавиши. Курсор позиционируется в конец сообщения.
     *
     * @param y вертикальное смещение строки, на которой выводить сообщение
     */
    public void waitForKey(int y) {
        String msg = "Press any key to continue...";
        writer.drawString(5, y, msg);
        screen.setCursorPosition(5 + msg.length() + 1, y);
        screen.refresh();
        readKey();
    }
}
